Предыдущая статья | Оглавление | Следующая статья |
Каждый уважающий себя администратор Linux должен уметь не только настраивать iptables, но и знать, как он работает. В этой статье речь пойдет не о том, как правильно настраивать iptables или какой-нибудь другой firewall, а о том, как работают firewall’ы в Linux.
В первую очередь, эта статья нацелена на читателей, которые занимаются (начинают или только хотят начать) программированием модулей ядра Linux (Linux Kernel Module – LKM), а также, надеюсь, поможет некоторым администраторам более детально разобраться в работе iptables. Все примеры в статье написаны для ядра 2.6.12.
Что такое netfilter и какое отношение он имеет к firewall’ам? В основном, Netfilter представляет собой набор функций (hook) расположенных в ядре, при помощи которых firewall’ы могут получать доступ к пакетам и, основываясь на правилах, решать, как с ними поступать дальше. Netfilter содержит 5 основных hook-функций, которые описаны в linux/netfilter_ipv4.h. Графически их можно изобразить так:
[INPUT]--->[1]--->[ROUTE]--->[3]---->[4]--->[OUTPUT] | ^ | | | [ROUTE] v | [2] [5] | ^ | | v | [INPUT*] [OUTPUT*]
Описание:
[1] NF_IP_PRE_ROUTING – наша функция срабатывает, как только мы получаем пакет, даже если он проходящий. Если мы хотим иметь доступ ко всем пакетам, проходящим через наш интерфейс, то мы должны использовать эту функцию.
[2] NF_IP_LOCAL_IN – срабатывает в случае, когда пакет адресован нам, перед поступлением его в сетевой стек.
[3] NF_IP_FORWARD – если пакет необходимо смаршрутизировать с одного интерфейса на другой.
[4] NF_IP_POST_ROUTING – для исходящих пакетов из нашего сетевого стека.
[5] NF_IP_LOCAL_OUT – для всех исходящих пакетов.
После вызова функции и проведения нехитрых проверок над пакетом, нам нужно вынести вердикт, что делать с этим пакетом дальше. В нашем распоряжении 5 вариантов:
[1] NF_ACCEPT – пропускает пакет дальше.
[2] NF_DROP – отбрасывает пакет.
[3] NF_REPEAT – повторный вызов функции.
[4] NF_STOLEN – забирает пакет (прекращается передвижение).
[5] NF_QUEUE – ставит пакет в очередь, как правило, для передачи в пользовательское пространство (мы ведь работаем в пространстве ядра).
Вот собственно и все, что нужно для нормальной работы любого firewall’а в Linux. С одной стороны, набор функций, позволяющий получать доступ к пакетам практически в любой точке сетевого стека, а с другой, набор решений, как поступить с пакетом дальше.
/* * Я думаю что администраторам дальше можно не читать, там пойдет объяснение структур, правильность * их заполнения, а также примеры использования. Вся теория работы firewall’а заканчивается. */
Теперь попытаемся разобраться, как все это работает! Первым делом нам нужно познакомиться со структурой nf_hook_ops, она и будет нашим проводником в мир netfilter’a. Описание её можно найти в /Linux/netfilter.h:
44 struct nf_hook_ops 45 { 46 struct list_head list; 47 48 /* User fills in from here down. */ 49 nf_hookfn *hook; 50 int pf; 51 int hooknum; 52 /* Hooks are ordered in ascending priority. */ 53 int priority; 54 };
Первое что мы видим, это “struct list_head list” – это структура, которая содержит список всех hook-функций, но нас она не сильно интересует.
nf_hookfn *hook – указатель на нашу функцию, в которой мы будем проводить все наши проверки. Возвращаемое значение должно быть одно из 5-и поведений (NF_ACCEPT, NF_DROP, ...).
int pf – служит для определения протокола, с которым мы хотим работать (PF_INET)
int hooknum – а вот и место нашего вызова. (например NF_IP_PRE_ROUTING)
int priority – приоритет. В случае если определено несколько функций на один вызов, первым сработает тот, у кого выше приоритет. Мы будем использовать – NF_IP_PRI_FIRST.
Не поверите, но это все! Остается лишь маленькое дополнение. После того как мы объявим и заполним нашу структуру, её необходимо зарегистрировать. Для этого служат 2-е функции, которые объявлены все в том же /Linux/netfilter.h:
89 /* Function to register/unregister hook points. */ 90 int nf_register_hook(struct nf_hook_ops *reg); 91 void nf_unregister_hook(struct nf_hook_ops *reg);
Первая из них - это nf_register_hook, служит для регистрации нашей hook-функции. А nf_unregister_hook – для удаления нашей функции из цепочки.
Ничего особенного, просто банальное предупреждение. Обязательно выгружайте ваши функции при выгрузке модуля из памяти при помощи nf_unregister_hook. Если этого не сделать, произойдет очень неприятная вещь. Придет пакет, сработает наш вызов, ядро попытается обратиться к странице памяти для вызова нашей функции для обработки, а там…. эээ в лучшем случае ничего, в худшем ..кто-то занял наше место и тогда результат буде непредсказуем.
Для примера напишем маленький firewall. Который будет беспощадно уничтожать все входящие и исходящие пакеты.
bash$ > cat firewall.c #include <linux/kernel.h> #include <linux/module.h> #include <linux/netfilter_ipv4.h> #include <linux/netfilter.h> MODULE_LICENSE ("GPL v2"); MODULE_AUTHOR ("ill"); MODULE_DESCRIPTION ("Firewall"); /* * Объявление структур. Мы объявим 2-е структуры. * 1-я для входящих пакетов * 2-я для исходящих пакетов */ struct nf_hook_ops nf_incoming; struct nf_hook_ops nf_outgoing; int i=1; unsigned int main_hook (unsigned int hooknum, struct sk_buff **skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { /* Для примера, мы будем отбрасывать все пакеты */ return NF_DROP; } int init_module (void) { /* * Заполнение структур * Сначала, заполним структуру для входящих пакетов */ nf_incoming.hook=main_hook; nf_incoming.pf=PF_INET; nf_incoming.hooknum=NF_IP_PRE_ROUTING; nf_incoming.priority=NF_IP_PRI_FIRST; /* Теперь для исходящих */ nf_outgoing.hook=main_hook; nf_outgoing.pf=PF_INET; nf_outgoing.hooknum=NF_IP_POST_ROUTING; nf_outgoing.priority=NF_IP_PRI_FIRST; /* Вроде все, осталось только зарегистрировать наши функции */ nf_register_hook(&nf_incoming); nf_register_hook(&nf_outgoing); printk ("FireWall loaded\n"); return 0; } void cleanup_module (void) { /* Не забываем удалить наши вызовы :), а то конфуз может случиться */ nf_unregister_hook(&nf_incoming); nf_unregister_hook(&nf_outgoing); printk ("FireWall unload\n"); }
Ну вот. Все очень просто! Теперь компилируем наш модуль, для этого я пользуюсь вот таким Makefile'ом: (предположим, что исходный код сохраним с именем firewall.c, а исходники ядра находится в папке /usr/src/linux).
obj-m += firewall.o all: make -C /usr/src/linux/ M=${PWD} modules clean: make -C /usr/src/linux/ M=${PWD} clean
И запускаем: insmod firewall.ko (иногда приходится запускать с ключом -f: insmod -f firewall.ko, а то ему версии не нравятся, но кому не лень, можно в модуле прописать все данные о версии ядра :). Посмотрите /var/log/messages – если увидите "FireWall loaded", значит наш модуль загрузился. Теперь, если вы попробуете подключиться к кому-нибудь, или наоборот, кто-то захочет к вам подключиться, ничего не выйдет. Наш модуль не пропустит ни одного пакета. Чтобы вернуть все на место, просто выгрузите модуль командой rmmod firewall.
Вот пример firewall’a в 60 строк, включая заголовки. Не сложно, правда..!!! :) Теперь перейдем к более сложным вещам. Но совсем на чуть-чуть.
В этом примере мы будем проводить небольшой анализ захваченного нами пакета. Наша программа будет анализировать заголовки пакета и в случае неудовлетворения правилам будет удалять его или править. Итак, для начала, небольшое введение в структуру sk_buff:
sk_buff – это буфер для работы с пакетами. Как только приходит пакет или появляется необходимость его отправить, создается sk_buff куда и помещается пакет, а также сопутствующая информация, откуда, куда, для чего… На протяжении всего путешествия пакета в сетевом стеке используется sk_buff. Как только пакет отправлен или данные переданы пользователю, структура уничтожается, тем самым освобождая память. Описание этой структуры можно найти в linux/skbuff.h. Она очень большая, я не буду выкладывать её
сюда :) Все, что мы будем использовать из неё, это:
Protocol – чтобы знать, с каким протоколом сетевого уровня мы имеем дело.
Data – место, где лежит пакет.
Более подробно о работе sk_buff можно почитать в Интернете, информации о нем море, а что касается практической части, советую почитать статью "Building Into The Linux Network Layer" из phrack №55
Ну вроде все, с теорией маленько разобрались. Теперь определимся, что и как мы будем делать. Так как это лишь пример использования, я не буду заострять внимание над нормализацией конкретного протокола, просто пробежимся немного по протоколам и все:
Type=0 Code=0 – ответ (echo_reply) на ping Type=3 Code=0–15 -назначение недоступно (сеть, хост, порт…) Type=8 Code=0 – запрос (echo) на ping
bash$ > cat norm.c #include <linux/module.h> /* Эйй, мы ведь пишем модуль к ядру */ #include <linux/kernel.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> /*подключаем заголовки для работы с сетевыми протоколами */ /* и собственно говоря sk_buff */ #include <linux/skbuff.h> #include <linux/inet.h> #include <linux/ip.h> #include <net/ip.h> #include <linux/tcp.h> #include <linux/icmp.h> #include <asm/uaccess.h> /* Увековечим свое имя Aha-ha… */ MODULE_LICENSE ("GPL v2"); MODULE_AUTHOR ("ill"); MODULE_DESCRIPTION ("Firewall"); struct nf_hook_ops nf_incoming; struct sk_buff *skbf; struct tcphdr *th; struct icmphdr *icmph; struct iphdr *iph; unsigned int main_hook (unsigned int hooknum, struct sk_buff **skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff*)) { skbf=*skb; /* Работаем только с IP */ if (skbf->protocol != htons(ETH_P_IP)) return NF_ACCEPT; /* Пропускаем только ICMP, TCP & UDP */ if (skbf->nh.iph->protocol != IPPROTO_TCP && skbf->nh.iph->protocol != IPPROTO_ICMP && skbf->nh.iph->protocol != IPPROTO_UDP) return NF_DROP; /* Проверка поля ttl */ if (ntohs(skbf->nh.iph->ttl)<10) { skbf->nh.iph->ttl=htons(100); ip_send_check(skbf->nh.iph); /* подсчет checksum */ return NF_ACCEPT; } /* Проверка ICMP, что мы здесь делаем, я думаю вы знаете :) */ if (skbf->nh.iph->protocol==IPPROTO_ICMP) { skbf->h.icmph=(struct icmphdr *)(skbf->data+(skbf->nh.iph->ihl*4)); if (skbf->h.icmph->type==ICMP_ECHOREPLY && skbf->h.icmph->code==0) return NF_ACCEPT; if (skbf->h.icmph->type==ICMP_DEST_UNREACH) return NF_ACCEPT; if (skbf->h.icmph->type==ICMP_ECHO && skbf->h.icmph->code==0) return NF_ACCEPT; return NF_DROP; } /* Блокируем TCP, если порт источника или назначения 31337, и при этом делаем запись */ /* в messages */ if (skbf->nh.iph->protocol==IPPROTO_TCP) { skbf->h.th=(struct tcphdr *)(skbf->data+(skbf->nh.iph->ihl*4)); if (skbf->h.th->dest==htons(31337) || skbf->h.th->source==htons(31337)) { printk ("Hacking attempt :) Good bye, young kiddies\n"); return NF_DROP; } return NF_ACCEPT; } /* Хех, если все прошло гладко, и никто не попал под наш мини-нормализатор */ /* милости просим в сетевой стек!!! */ return NF_ACCEPT; } int init_module () { nf_incoming.hook = main_hook; nf_incoming.pf = PF_INET; nf_incoming.hooknum = NF_IP_PRE_ROUTING; nf_incoming.priority = NF_IP_PRI_FIRST; nf_register_hook(&nf_incoming); printk ("FireWall loaded\n"); return 0; } void cleanup_module () { nf_unregister_hook(&nf_incoming); printk ("FireWall unload\n"); }
Ну вот и все, ребята. Теперь вы знаете, как работают firewall’ы в Linux и даже сможете написать свой собственный, знаете для чего служит netfilter и познакомились немного с работой сетевого стека ядра. Как видите, ничего сложного. Пару строк кода, и вы получаете доступ к святая святых, вы можете вертеть протоколами как угодно, менять их заголовки, отслеживать неприятеля и многое-многое другое…
P.S. Может кто подскажет, а как в Windows работает fireWall? :)
Предыдущая статья | Оглавление | Следующая статья |